using SS.Common.Lock;

namespace SS.Common.Collection
{
    #region Used namespaces

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;

    #endregion

    /// <summary>
    /// Blocking collection by wrapping a concurrent collection and using lock wait
    /// </summary>
    /// <typeparam name="T">
    /// type of collection item
    /// </typeparam>
    public class WaitBlockingCollection<T> : IEnumerable<T>, ICollection, IDisposable
    {
        /// <summary>
        /// Underlying collection
        /// </summary>
        private readonly IConcurrentCollection<T> underlyingColl;

        /// <summary>
        /// Maximum number of item
        /// </summary>
        private readonly int upperBound;

        private readonly Action waitForRemove;

        private readonly Func<int, bool> tryWaitForRemove;

        private readonly Action notifyRemoved;

        /// <summary>
        /// Mark the collection that it is complete
        /// </summary>
        private AtomicBoolean isComplete;

        /// <summary>
        /// Semaphore guarding removing item
        /// </summary>
        private readonly Semaphore removeSemaphore;

        /// <summary>
        /// Semaphore guarding add item
        /// </summary>
        private readonly Semaphore addSemaphore;

        private bool disposed = false;

        #region ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="WaitBlockingCollection{T}"/> class. 
        /// Wrapping a ConcurrentQueue underlying
        /// </summary>
        public WaitBlockingCollection()
            : this(new ConcurrentQueueEx<T>(), -1)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="WaitBlockingCollection{T}"/> class. 
        /// </summary>
        /// <param name="upperBound">
        /// Maximum number of item can be stored, -1 mean no limit
        /// </param>
        public WaitBlockingCollection(int upperBound)
            : this(new ConcurrentQueueEx<T>(), upperBound)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="WaitBlockingCollection{T}"/> class. 
        /// </summary>
        /// <param name="underlyingColl">
        /// Underlining collection
        /// </param>
        public WaitBlockingCollection(IConcurrentCollection<T> underlyingColl)
            : this(underlyingColl, -1)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="WaitBlockingCollection{T}"/> class. 
        /// </summary>
        /// <param name="underlyingColl">
        /// Underlying collection
        /// </param>
        /// <param name="upperBound">
        /// Maximum allowed number of items
        /// </param>
        public WaitBlockingCollection(IConcurrentCollection<T> underlyingColl, int upperBound)
        {
            this.underlyingColl = underlyingColl;
            this.upperBound = upperBound;
            this.removeSemaphore = new Semaphore(0, int.MaxValue);
            if (upperBound == -1)
            {
                // this.isFull = delegate { return false; };
                this.addSemaphore = null;
                this.waitForRemove = delegate { };
                this.tryWaitForRemove = delegate { return true; };
                this.notifyRemoved = delegate { };
            }
            else
            {
                // this.isFull = delegate { return underlyingColl.Count >= upperBound; };
                this.addSemaphore = new Semaphore(this.upperBound, this.upperBound);
                this.waitForRemove = delegate { this.addSemaphore.WaitOne(); };
                this.tryWaitForRemove =
                    waitMillies => { return this.addSemaphore.WaitOne(waitMillies); };
                this.notifyRemoved = delegate { this.addSemaphore.Release(); };
            }
        }

        /// <summary>
        /// Finalizes an instance of the <see cref="WaitBlockingCollection{T}"/> class. 
        /// Call to dispose
        /// </summary>
        ~WaitBlockingCollection()
        {
            this.Dispose(true);
        }

        #endregion

        #region Add & Remove (+ Try)

        /// <summary>
        /// Add a new item to the collection
        /// Check for full or completed before add
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <exception cref="InvalidOperationException">
        /// If is full or completed
        /// </exception>
        public void Add(T item)
        {
            if (this.IsAddingCompleted)
            {
                throw new InvalidOperationException(
                    "The BlockingCollection<T> has been marked as complete with regards to additions.");
            }

            this.waitForRemove();
            this.underlyingColl.Add(item);
            this.removeSemaphore.Release();
        }

        /// <summary>
        /// Remove the last item
        /// </summary>
        /// <returns>
        /// The last item
        /// </returns>
        public T Remove()
        {
            this.removeSemaphore.WaitOne();
            T item;
            this.underlyingColl.Remove(out item);
            this.notifyRemoved();
            return item;
        }

        /// <summary>
        /// Try to add an item
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <returns>
        /// true if add successfully
        /// </returns>
        public bool TryAdd(T item)
        {
            return this.TryAdd(item, 0);
        }

        /// <summary>
        /// Try to add an item within a specified ammount of time
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="ts">
        /// The specified ammount of time
        /// </param>
        /// <returns>
        /// true if add successfully
        /// </returns>
        public bool TryAdd(T item, TimeSpan ts)
        {
            return this.TryAdd(item, (int)ts.TotalMilliseconds);
        }

        /// <summary>
        /// Try to add an item within a specified ammount of time
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="millisecondsTimeout">
        /// Ammount of time in milliseconds
        /// </param>
        /// <returns>
        /// true if add successfully
        /// </returns>
        public bool TryAdd(T item, int millisecondsTimeout)
        {
            if (this.isComplete.Value)
            {
                return false;
            }

            if (this.tryWaitForRemove(millisecondsTimeout))
            {
                this.underlyingColl.Add(item);
                this.removeSemaphore.Release();
                return true;
            }

            return false;
        }

        /// <summary>
        /// Try to remove an item by blocking wait
        /// </summary>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <returns>
        /// true if remove successfully
        /// </returns>
        public bool TryRemove(out T item)
        {
            return this.TryRemove(out item, 0);
        }

        /// <summary>
        /// Try to remove an item within a specified ammount of time
        /// </summary>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <param name="ts">
        /// The specified ammount of time
        /// </param>
        /// <returns>
        /// true if remove successfully
        /// </returns>
        public bool TryRemove(out T item, TimeSpan ts)
        {
            return this.TryRemove(out item, (int)ts.TotalMilliseconds);
        }

        /// <summary>
        /// Try to add an item within a specified ammount of time
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="millisecondsTimeout">
        /// Ammount of time in milliseconds
        /// </param>
        /// <returns>
        /// true if add successfully
        /// </returns>
        public bool TryRemove(out T item, int millisecondsTimeout)
        {
            if (this.removeSemaphore.WaitOne(millisecondsTimeout))
            {
                this.underlyingColl.Remove(out item);
                this.notifyRemoved();
                return true;
            }

            item = default(T);
            return false;
        }

        #endregion

        #region static methods

        /// <summary>
        /// Check array for null or contains invalid data (null items)
        /// </summary>
        /// <param name="collections">
        /// Collections array for checking
        /// </param>
        private static void CheckArray(ICollection<BlockingCollection<T>> collections)
        {
            if (collections == null)
            {
                throw new ArgumentNullException("collections");
            }

            if (collections.Count == 0 || collections.Where(e => e == null).Any())
            {
                throw new ArgumentException(
                    "The collections argument is a 0-length array or contains a null element.",
                    "collections");
            }
        }

        /// <summary>
        /// Add an item to any collection in the array
        /// </summary>
        /// <param name="collections">
        /// Array of collections to add the item to
        /// </param>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <returns>
        /// Index of the chosen array
        /// </returns>
        public static int AddAny(BlockingCollection<T>[] collections, T item)
        {
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                try
                {
                    coll.Add(item);
                    return index;
                }
                catch (InvalidOperationException)
                {
                    // Ignore exception as expected
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to add an item to any collection in the array 
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to add item to
        /// </param>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryAddAny(BlockingCollection<T>[] collections, T item)
        {
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryAdd(item))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to add an item to any collection in the array in a specificed ammount of time
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to add item to
        /// </param>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="ts">
        /// The specified ammount of time
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryAddAny(BlockingCollection<T>[] collections, T item, TimeSpan ts)
        {
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryAdd(item, ts))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to add an item to any collection in the array in a specificed ammount of time
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to add item to
        /// </param>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="millisecondsTimeout">
        /// The specified ammount of time in ms
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryAddAny(
            BlockingCollection<T>[] collections, T item, int millisecondsTimeout)
        {
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryAdd(item, millisecondsTimeout))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Remove an item to any collection in the array
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to remove item
        /// </param>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int RemoveAny(BlockingCollection<T>[] collections, out T item)
        {
            item = default(T);
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                try
                {
                    item = coll.Remove();
                    return index;
                }
                catch (InvalidOperationException)
                {
                    // Expected exception
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to remove an item to any collection in the array
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to remove item
        /// </param>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryRemoveAny(BlockingCollection<T>[] collections, out T item)
        {
            item = default(T);

            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryRemove(out item))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to remove an item to any collection in the array in a specified ammount of time
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to remove item
        /// </param>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <param name="ts">
        /// The specified ammount of time
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryRemoveAny(BlockingCollection<T>[] collections, out T item, TimeSpan ts)
        {
            item = default(T);

            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryRemove(out item, ts))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to remove an item to any collection in the array in a specified ammount of time
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to remove item
        /// </param>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <param name="millisecondsTimeout">
        /// The specified ammount of time in ms
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryRemoveAny(
            BlockingCollection<T>[] collections, out T item, int millisecondsTimeout)
        {
            item = default(T);

            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryRemove(out item, millisecondsTimeout))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        #endregion

        /// <summary>
        /// Mark the collection to prevent adding new item
        /// </summary>
        public void CompleteAdding()
        {
            this.isComplete = true;
        }

        /// <summary>
        /// Copy items to an array
        /// </summary>
        /// <param name="array">
        /// The destination array
        /// </param>
        /// <param name="index">
        /// Copy only from this index
        /// </param>
        void ICollection.CopyTo(Array array, int index)
        {
            this.underlyingColl.CopyTo(array, index);
        }

        /// <summary>
        /// Copy items to an array
        /// </summary>
        /// <param name="array">
        /// The destination array
        /// </param>
        /// <param name="index">
        /// Copy only from this index
        /// </param>
        public void CopyTo(T[] array, int index)
        {
            this.underlyingColl.CopyTo(array, index);
        }

        /// <summary>
        /// Get the consuming enumerable
        /// </summary>
        /// <returns>
        /// The enumerable
        /// </returns>
        public IEnumerable<T> GetConsumingEnumerable()
        {
            T item;
            while (this.TryRemove(out item))
            {
                yield return item;
            }
        }

        /// <summary>
        /// Get items enumerator
        /// </summary>
        /// <returns>
        /// The enumerator
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        /// <summary>
        /// Get items enumerator
        /// </summary>
        /// <returns>
        /// The enumerator
        /// </returns>
        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        /// <summary>
        /// Get items enumerator
        /// </summary>
        /// <returns>
        /// The enumerator
        /// </returns>
        public IEnumerator<T> GetEnumerator()
        {
            return this.GetConsumingEnumerable().GetEnumerator();
        }

        /// <summary>
        /// Dispose the object
        /// </summary>
        public void Dispose()
        {
            this.Dispose(!this.disposed);
        }

        /// <summary>
        /// Dispose any managed resource if there is
        /// Mean for overiding by subclass
        /// </summary>
        /// <param name="managedRes">
        /// Clean up the managed resource
        /// </param>
        protected virtual void Dispose(bool managedRes)
        {
            if (managedRes)
            {
                if (this.addSemaphore != null)
                {
                    this.addSemaphore.Close();
                }

                if (this.removeSemaphore != null)
                {
                    this.removeSemaphore.Close();
                }

                this.disposed = true;
            }
        }

        /// <summary>
        /// Convert to array
        /// </summary>
        /// <returns>
        /// The returned array
        /// </returns>
        public T[] ToArray()
        {
            return this.underlyingColl.ToArray();
        }

        /// <summary>
        /// Gets predefined upper bound for the collection
        /// </summary>
        public int BoundedCapacity
        {
            get
            {
                return this.upperBound;
            }
        }

        /// <summary>
        /// Gets current items count
        /// </summary>
        public int Count
        {
            get
            {
                return this.underlyingColl.Count;
            }
        }

        /// <summary>
        /// Is completed mark set
        /// </summary>
        public bool IsAddingCompleted
        {
            get
            {
                return this.isComplete.Value;
            }
        }

        /// <summary>
        /// Is completed mark set and no item stored in the collection
        /// </summary>
        public bool IsCompleted
        {
            get
            {
                return this.isComplete.Value && this.underlyingColl.Count == 0;
            }
        }

        /// <summary>
        /// Gets SyncRoot object
        /// </summary>
        /// <value>
        /// The sych object
        /// </value>
        object ICollection.SyncRoot
        {
            get
            {
                return this.underlyingColl.SyncRoot;
            }
        }

        /// <summary>
        /// Indicate this collection is thread safe
        /// Delegate to the underlying collection
        /// </summary>
        bool ICollection.IsSynchronized
        {
            get
            {
                return this.underlyingColl.IsSynchronized;
            }
        }
    }
}
